﻿
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Xunit;
using Xunit.Extensions;

namespace EmperialApps.WeatherSpark.Data {

    public class TestLocationManager {

        [Theory]
        [InlineData( "", null )]
        [InlineData( "12345", null )]
        [InlineData( "Austin", new[] { "Austin" } )]
        [InlineData( "Austin, TX", new[] { "Austin", "TX" } )]
        [InlineData( "New York City", new[] { "New", "York", "City" } )]
        public void GetSearchTerms_GivenSearchString_ReturnsAlphabeticalWords( string search, string[] expected ) {
            expected = expected ?? new string[0];

            string[] actual = LocationManager.GetSearchTerms( search );

            Assert.Equal( expected, actual );
        }

        [Theory]
        [InlineData( "12345", null, true )]
        [InlineData( "Austin", null, true )]
        [InlineData( "Austin", new[] { "AUS" }, true )]
        [InlineData( "Austin", new[] { "Austin" }, true )]
        [InlineData( "Austinville", new[] { "Austin" }, true )]
        [InlineData( "Austin", new[] { "Austinville" }, false )]
        [InlineData( "Austin, TX", new[] { "Austin", "TX" }, true )]
        [InlineData( "Austin, IL", new[] { "Austin", "TX" }, false )]
        public void IsMatch_ReturnsExpectedValue( string search, string[] terms, bool expected ) {
            terms = terms ?? new string[0];

            bool actual = LocationManager.IsMatch( search, terms );

            Assert.Equal( expected, actual );
        }


        [Fact]
        public void TryGetLocations_GivenExistingSearch_ReturnsSavedPlaces( ) {
            string search = "12345";
            var expected = Pair.Create( true, new[] { GetPlace( "city" ) } );
            var store = new MockStore { Locations = { { search, expected.Item2 } } };
            var manager = CreateManager( store );

            Place[] places;
            var actual = Pair.Create( manager.TryGetLocations( search, out places ), places );

            AssertTryGetLocations( expected, actual );
        }

        [Fact]
        public void TryGetLocations_GivenSearchWithMatchingPrecedents_ReturnsAggregatedPlaces( ) {
            string search = "city";
            var expected = Pair.Create( true, new[] { GetPlace( "city 1" ), GetPlace( "city 2" ), GetPlace( "city 3" ) } );
            var store = new MockStore {
                Locations = {
                    { "cit", new[] { expected.Item2[1], expected.Item2[2] } },
                    { "city", new[] { expected.Item2[0] } },
                }
            };
            var manager = CreateManager( store );

            Place[] places;
            var actual = Pair.Create( manager.TryGetLocations( search, out places ), places );

            AssertTryGetLocations( expected, actual );
        }

        [Fact]
        public void TryGetLocations_GivenSearchWithIdenticalPrecedents_ReturnsDistinctAggregatedPlaces( ) {
            string search = "city";
            var expected = Pair.Create( true, new[] { GetPlace( "city 1", "county" ), GetPlace( "city 2", "county" ) } );
            var store = new MockStore {
                Locations = {
                    { "cit", new[] { expected.Item2[1], GetPlace( expected.Item2[0].Name ) } },
                    { "city", new[] { expected.Item2[0] } },
                }
            };
            var manager = CreateManager( store );

            Place[] places;
            var actual = Pair.Create( manager.TryGetLocations( search, out places ), places );

            AssertTryGetLocations( expected, actual );
        }

        [Fact]
        public void TryGetLocations_GivenSearchWithNonMatchingPrecedents_ReturnsAggregatedPlaces( ) {
            string search = "city";
            var expected = Pair.Create( true, new[] { GetPlace( "city 1" ), GetPlace( "city 2" ) } );
            var store = new MockStore {
                Locations = {
                    { "cit", new[] { expected.Item2[1], GetPlace( "non-matching" ) } },
                    { "city", new[] { expected.Item2[0] } },
                }
            };
            var manager = CreateManager( store );

            Place[] places;
            var actual = Pair.Create( manager.TryGetLocations( search, out places ), places );

            AssertTryGetLocations( expected, actual );
        }


        [Fact]
        public void TryGetLocations_GivenNewSearch_ReturnsFalseAndEmptyPlaces( ) {
            var expected = Pair.Create( false, new Place[0] );
            string search = "search";
            var store = new MockStore( );
            var manager = CreateManager( store );

            Place[] places;
            var actual = Pair.Create( manager.TryGetLocations( search, out places ), places );

            AssertTryGetLocations( expected, actual );
        }

        [Fact]
        public void TryGetLocations_GivenNewSearchWithMatchingPrecedents_ReturnsFalseAndAggregatedPlaces( ) {
            var expected = Pair.Create( false, new[] { GetPlace( "preceding city" ) } );
            string search = "city";
            var store = new MockStore { Locations = { { "cit", expected.Item2 }, } };
            var manager = CreateManager( store );

            Place[] places;
            var actual = Pair.Create( manager.TryGetLocations( search, out places ), places );

            AssertTryGetLocations( expected, actual );
        }


        [Fact]
        public void Download_GivenNewSearch_BeginsDownload( ) {
            var expected = new[] { "search" };
            var store = new MockStore( );
            var manager = CreateManager( store );

            manager.DownloadLocations( expected[0] );
            var actual = store.Downloads;

            Assert.Equal( expected, actual );
        }

        [Fact]
        public void Download_GivenNewSearchMultipleTimes_BeginsDownloadOnce( ) {
            var expected = new[] { "search" };
            var store = new MockStore( );
            var manager = CreateManager( store );

            manager.DownloadLocations( expected[0] );
            manager.DownloadLocations( expected[0] );
            var actual = store.Downloads;

            Assert.Equal( expected, actual );
        }

        [Fact]
        public void Download_GivenStoredSearch_DoesNotBeginDownload( ) {
            var store = new MockStore( );
            string search = store.Locations.Keys.First( );
            var manager = CreateManager( store );

            manager.DownloadLocations( search );
            var actual = store.Downloads;

            Assert.Empty( actual );
        }


        [Fact]
        public void DownloadCompleted_ForNewSearch_SavesDownloadToStore( ) {
            string search = "City ST";
            var expected = new[] { GetPlace( search ) };
            var store = new MockStore( );
            var manager = CreateManager( store );

            manager.DownloadLocations( search );
            store.DownloadCompletedHandlers( store, new LocationEventArgs( search, expected ) );
            var actual = store.Locations[search];

            Assert.Single( store.Downloads, search );
            Assert.Equal( expected, actual );
        }

        [Theory]
        [InlineData( typeof( FormatException ) )]
        [InlineData( typeof( System.Net.WebException ) )]
        public void DownloadCompleted_WhenDownloadFailsWithException_SavesDownloadToStoreWithoutModfication( Type exceptionType ) {
            var exception = (Exception)Activator.CreateInstance( exceptionType );
            string search = "search";
            var store = new MockStore( );
            var manager = CreateManager( store );

            var expected = new LocationEventArgs( search, null, exception );
            store.DownloadCompletedHandlers( store, expected );
            var actual = store.Saves.Single( );

            Assert.Same( expected, actual );
        }


        [Fact]
        public void DownloadCompleted_ForZeroSearchResults_WhenSearchHasAlternateForm_BeginsNewDownload( ) {
            string[] searches = new[] { "City ST", "City, ST" };
            var places = new Place[0];
            var store = new MockStore( );
            var progress = new List<Pair<int, Pair<string, string>>>( );
            var manager = CreateManager( store, progress );

            manager.DownloadLocations( searches[0] );
            store.DownloadCompletedHandlers( store, new LocationEventArgs( searches[0], places ) );

            Assert.Empty( progress );
            Assert.Equal( searches, store.Downloads );
        }

        [Theory]
        [InlineData( 0 )]
        [InlineData( 1 )]
        [InlineData( 2 )]
        public void DownloadCompleted_ForAlternateSearchResults_RaisesProgressEventForOriginalSearch( int results ) {
            string[] searches = new[] { "City ST", "City, ST" };
            var places = Enumerable.Range( 0, results ).Select( i => GetPlace( "Result " + i ) ).ToArray( );
            var store = new MockStore( );
            var progress = new List<Pair<int, Pair<string, string>>>( );
            var manager = CreateManager( store, progress );

            manager.DownloadLocations( searches[0] );
            store.DownloadCompletedHandlers( store, new LocationEventArgs( searches[0], new Place[0] ) );
            store.DownloadCompletedHandlers( store, new LocationEventArgs( searches[1], places ) );

            Assert.Equal( searches, store.Downloads );
            var p = Assert.Single( progress );
            Assert.Equal( places.Length, p.Item1 );
            Assert.Contains( searches[0], p.Item2.Item1 );
        }

        [Fact]
        public void DownloadCompleted_ForAlternateSearchResults_WithInterveningSearch_DoesNotRaiseProgressEvent( ) {
            string[] searches = new[] { "City ST", "Intervening", "City, ST" };
            var places = new Place[0];
            var store = new MockStore( );
            var progress = new List<Pair<int, Pair<string, string>>>( );
            var manager = CreateManager( store, progress );

            manager.DownloadLocations( searches[0] );
            manager.DownloadLocations( searches[1] );
            store.DownloadCompletedHandlers( store, new LocationEventArgs( searches[0], places ) );
            store.DownloadCompletedHandlers( store, new LocationEventArgs( searches[2], places ) );

            Assert.Equal( searches, store.Downloads );
            Assert.Empty( progress );
        }

        [Theory]
        [InlineData( typeof( FormatException ), "Format error" )]
        [InlineData( typeof( System.Net.WebException ), "Web error" )]
        public void DownloadCompleted_WhenDownloadFailsWithException_RaisesProgressEvent( Type exceptionType, string errorName ) {
            var exception = (Exception)Activator.CreateInstance( exceptionType );
            string search = "search";
            var store = new MockStore( );
            var progress = new List<Pair<int, Pair<string, string>>>( );
            var manager = CreateManager( store, progress );

            manager.DownloadLocations( search );
            store.DownloadCompletedHandlers( store, new LocationEventArgs( search, null, exception ) );
            var actual = store.Saves.Single( );

            var p = Assert.Single( progress );
            Assert.Equal( 0, p.Item1 );
            Assert.Equal( search, p.Item2.Item1 );
            Assert.Equal( errorName, p.Item2.Item2 );
        }

        [Fact]
        public void DownloadCompleted_ForZeroSearchResults_RaisesProgressEvent( ) {
            string search = "search";
            var places = new Place[0];
            var store = new MockStore( );
            var progress = new List<Pair<int, Pair<string, string>>>( );
            var manager = CreateManager( store, progress );

            manager.DownloadLocations( search );
            store.DownloadCompletedHandlers( store, new LocationEventArgs( search, places ) );

            Assert.Single( store.Downloads, search );
            var p = Assert.Single( progress );
            Assert.Equal( places.Length, p.Item1 );
            Assert.Equal( search, p.Item2.Item1 );
        }

        [Fact]
        public void DownloadCompleted_ForSingleSearchResult_RaisesProgressEvent( ) {
            string search = "search";
            var places = new[] { GetPlace( search ) };
            var store = new MockStore( );
            var progress = new List<Pair<int, Pair<string, string>>>( );
            var manager = CreateManager( store, progress );

            manager.DownloadLocations( search );
            store.DownloadCompletedHandlers( store, new LocationEventArgs( search, places ) );

            var p = Assert.Single( progress );
            Assert.Equal( places.Length, p.Item1 );
            Assert.Equal( search, p.Item2.Item1 );
        }

        [Fact]
        public void DownloadCompleted_ForMultipleSearchResults_RaisesProgressEvent( ) {
            string search = "search";
            var places = new[] { GetPlace( "city 1" ), GetPlace( "city 2" ) };
            string expected = search;
            var store = new MockStore( );
            var progress = new List<Pair<int, Pair<string, string>>>( );
            var manager = CreateManager( store, progress );

            manager.DownloadLocations( search );
            store.DownloadCompletedHandlers( store, new LocationEventArgs( search, places ) );

            var p = Assert.Single( progress );
            Assert.Equal( places.Length, p.Item1 );
            Assert.Equal( search, p.Item2.Item1 );
        }

        [Theory]
        [InlineData( true )]
        [InlineData( false )]
        public void DownloadCompleted_ForMultipleSearches_RaisesProgressEventForLastSearch( bool completeInOrder ) {
            string[] searches = new[] { "search 1", "search 2" };
            string[] complete = completeInOrder ? searches : new[] { searches[1], searches[0] };
            var store = new MockStore( );
            var progress = new List<Pair<int, Pair<string, string>>>( );
            var manager = CreateManager( store, progress );

            Array.ForEach( searches, manager.DownloadLocations );
            Array.ForEach( complete, search => store.DownloadCompletedHandlers( store, new LocationEventArgs( search, new[] { GetPlace( search ) } ) ) );

            var p = Assert.Single( progress );
            Assert.Equal( 1, p.Item1 );
            Assert.Contains( searches[1], p.Item2.Item1 );
        }


        #region Utility

        private static Place GetPlace( string name, string county = "" ) {
            return new Place( default( Coordinate ), name, county, null );
        }

        private static LocationManager CreateManager( MockStore store = null, List<Pair<int, Pair<string, string>>> progress = null ) {
            var manager = new LocationManager( store ?? new MockStore( ) );

            if( progress != null )
                manager.Progress += ( s, e ) => progress.Add( Pair.Create( e.ProgressPercentage, (Pair<string, string>)e.UserState ) );

            return manager;
        }

        private static void AssertTryGetLocations( Pair<bool, Place[]> expected, Pair<bool, Place[]> actual ) {
            Assert.Equal( expected.Item1, actual.Item1 );
            Assert.Equal( expected.Item2, actual.Item2 );
        }


        private sealed class MockStore : ILocationStore {
            public EventHandler<LocationEventArgs> DownloadCompletedHandlers;
            public readonly Dictionary<string, Place[]> Locations;
            public readonly List<string> Downloads;
            public readonly List<string> Loads;
            public readonly List<LocationEventArgs> Saves;

            public MockStore( ) {
                this.Locations = new Dictionary<string, Place[]> { { "other", new[] { GetPlace( "other city" ) } } };
                this.Downloads = new List<string>( );
                this.Loads = new List<string>( );
                this.Saves = new List<LocationEventArgs>( );
            }

            public bool Save( LocationEventArgs e ) {
                this.Saves.Add( e );
                if( e.Error == null ) {
                    Place[] locations = e.Places;
                    this.Locations[e.Search] = locations;
                }

                return e.Error == null;
            }

            public Place[] Load( string search, object context ) {
                this.Loads.Add( search );

                Place[] locations;
                this.Locations.TryGetValue( search, out locations );
                return locations;
            }

            public event EventHandler<LocationEventArgs> DownloadCompleted {
                add { this.DownloadCompletedHandlers += value; }
                remove { this.DownloadCompletedHandlers -= value; }
            }

            public void BeginDownload( string search ) {
                this.Downloads.Add( search );
            }
        }

        #endregion
    }

}
